﻿using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

///
/// erstellt mit Hilfe von http://www.riemers.net/eng/Tutorials/XNA/Csharp/series1.php
///

namespace XNA_TD
{
    public struct VertexMultitextured
    {
        public Vector3 Position;
        public Vector3 Normal;
        public Vector2 TextureCoordinate;
        public Vector3 TextureWeight;

        public static int SizeInBytes = (3 + 3 + 2 + 3) * sizeof(float);
        public static VertexElement[] VertexElements = new VertexElement[]
        {
            new VertexElement(0, VertexElementFormat.Vector3,  VertexElementUsage.Position, 0 ),
            new VertexElement(sizeof(float) * 3, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0 ),
            new VertexElement(sizeof(float) * 6, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0 ),
            new VertexElement(sizeof(float) * 8, VertexElementFormat.Vector3, VertexElementUsage.TextureCoordinate, 1 ),
        };
        public static VertexDeclaration VertexDeclaration = new VertexDeclaration(VertexElements);
    }


    public class Terrain : IRunable, IDraw
    {
        public int terrainWidth, terrainHeight;
        public float[,] heightData;
        VertexMultitextured[] vertices;
        short[] indices;
        VertexBuffer vertexBuffer;
        IndexBuffer indexBuffer;
        Texture2D texture1, texture2, texture3;

        public Terrain()
            : base(Priority.Logic)
        {
            Texture2D heightMap = Content.Load<Texture2D>("Terrain/heightmap");
            texture1 = Content.Load<Texture2D>("Terrain/Gras");
            texture2 = Content.Load<Texture2D>("Terrain/Sand");
            texture3 = Content.Load<Texture2D>("Terrain/Rock");
            //LoadHeightDataBig(heightMap);
            LoadHeightData(heightMap);
            SetUpVertices();
            SetUpIndices();
            CalculateNormals();
            SetUpTextureWeight();
            CopyToBuffers();
            terrain = this;
        }

        private void LoadHeightData(Texture2D heightMap)
        {
            terrainWidth = heightMap.Width;
            terrainHeight = heightMap.Height;

            Color[] heightMapColors = new Color[terrainWidth * terrainHeight];
            heightMap.GetData(heightMapColors);

            heightData = new float[terrainWidth, terrainHeight];
            for (int x = 0; x < terrainWidth; x++)
                for (int y = 0; y < terrainHeight; y++)
                    heightData[x, y] = heightMapColors[x + y * terrainWidth].R / 255f * 4f;
        }

        private void LoadHeightDataBig(Texture2D heightMap)
        {
            terrainWidth = heightMap.Width;
            terrainHeight = heightMap.Height;

            Color[] heightMapColors = new Color[terrainWidth * terrainHeight];
            heightMap.GetData(heightMapColors);

            heightData = new float[terrainWidth * 2, terrainHeight * 2];
            for (int x = 0; x < terrainWidth; x++)
                for (int y = 0; y < terrainHeight; y++)
                    heightData[x * 2, y * 2] = heightMapColors[x + y * terrainWidth].R / 255f * 4f;

            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainHeight; y++)
                {
                    if (x * 2 + 1 < terrainHeight)
                    {
                        heightData[x * 2 + 1, y * 2] = (heightData[x * 2, y * 2] + heightData[x * 2 + 2, y * 2]) / 2f;
                    }
                    else
                    {
                        heightData[x * 2 + 1, y * 2] = heightData[x * 2, y * 2];
                    }
                    if (y * 2 + 1 < terrainWidth)
                    {
                        heightData[x * 2, y * 2 + 1] = (heightData[x * 2, y * 2] + heightData[x * 2, y * 2 + 2]) / 2f;
                    }
                    else
                    {
                        heightData[x * 2, y * 2 + 1] = heightData[x * 2, y * 2];
                    }
                    if (y * 2 + 1 < terrainWidth && x * 2 + 1 < terrainHeight)
                    {
                        heightData[x * 2 + 1, y * 2 + 1] = (heightData[x * 2, y * 2] + heightData[x * 2 + 2, y * 2] + heightData[x * 2, y * 2 + 2] + heightData[x * 2 + 2, y * 2 + 2]) / 4f;
                    }
                    else if (y * 2 + 1 < terrainWidth)
                    {
                        heightData[x * 2 + 1, y * 2 + 1] = (heightData[x * 2, y * 2] + heightData[x * 2, y * 2 + 2]) / 2f;
                    }
                    else if (x * 2 + 1 < terrainHeight)
                    {
                        heightData[x * 2 + 1, y * 2 + 1] = (heightData[x * 2, y * 2] + heightData[x * 2 + 2, y * 2]) / 2f;
                    }
                    else
                    {
                        heightData[x * 2 + 1, y * 2 + 1] = heightData[x * 2, y * 2];
                    }
                }
            }
            terrainWidth *= 2;
            terrainHeight *= 2;
        }

        private void SetUpVertices()
        {
            vertices = new VertexMultitextured[terrainWidth * terrainHeight];
            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainHeight; y++)
                {
                    vertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], y);
                    vertices[x + y * terrainWidth].TextureCoordinate = new Vector2((float)x / terrainWidth, (float)y / terrainHeight);
                }
            }
        }

        private void SetUpTextureWeight()
        {
            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainHeight; y++)
                {
                    float sand = MathHelper.Clamp(1f - (heightData[x, y] - .6f), 0, 1f); // Sand
                    float rock = MathHelper.Clamp(
                        (float)Math.Acos(Vector3.Dot(Vector3.Up, vertices[x + y * terrainWidth].Normal)) * 3 - .1f,
                        0,
                        1f - sand); // Rock
                    /*if (heightData[x, y] <= 1)
                        rock = 0;*/
                    rock = 0; // Varbverlauf sieht schlecht aus daher 0
                    float grass = MathHelper.Clamp((heightData[x, y] - .6f), 0, 1f - rock - sand); // Grass
                    //float sand = MathHelper.Clamp(1f - rock - grass, 0, 1); // Sand

                    vertices[x + y * terrainWidth].TextureWeight.X = grass;
                    vertices[x + y * terrainWidth].TextureWeight.Y = sand;
                    vertices[x + y * terrainWidth].TextureWeight.Z = rock;


                    float total = vertices[x + y * terrainWidth].TextureWeight.X;
                    total += vertices[x + y * terrainWidth].TextureWeight.Y;
                    total += vertices[x + y * terrainWidth].TextureWeight.Z;

                    vertices[x + y * terrainWidth].TextureWeight.X /= total;
                    vertices[x + y * terrainWidth].TextureWeight.Y /= total;
                    vertices[x + y * terrainWidth].TextureWeight.Z /= total;
                }
            }
        }

        private void SetUpIndices()
        {
            indices = new short[(terrainWidth - 1) * (terrainHeight - 1) * 6];
            int counter = 0;
            for (int y = 0; y < terrainHeight - 1; y++)
            {
                for (int x = 0; x < terrainWidth - 1; x++)
                {
                    int lowerLeft = x + y * terrainWidth;
                    int lowerRight = (x + 1) + y * terrainWidth;
                    int topLeft = x + (y + 1) * terrainWidth;
                    int topRight = (x + 1) + (y + 1) * terrainWidth;

                    indices[counter++] = (short) topLeft;
                    indices[counter++] = (short)lowerLeft;
                    indices[counter++] = (short)lowerRight;

                    indices[counter++] = (short)topLeft;
                    indices[counter++] = (short)lowerRight;
                    indices[counter++] = (short)topRight;
                }
            }
        }

        private void CalculateNormals()
        {
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            for (int i = 0; i < indices.Length / 3; i++)
            {
                int index1 = indices[i * 3];
                int index2 = indices[i * 3 + 1];
                int index3 = indices[i * 3 + 2];

                Vector3 side1 = vertices[index1].Position - vertices[index3].Position;
                Vector3 side2 = vertices[index1].Position - vertices[index2].Position;
                Vector3 normal = Vector3.Cross(side1, side2);

                vertices[index1].Normal += normal;
                vertices[index2].Normal += normal;
                vertices[index3].Normal += normal;
            }

            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();
        }
        
        private void CopyToBuffers()
        {
            vertexBuffer = new VertexBuffer(graphics.GraphicsDevice, VertexMultitextured.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly);
            vertexBuffer.SetData(vertices);

            indexBuffer = new IndexBuffer(graphics.GraphicsDevice, typeof(short), indices.Length, BufferUsage.WriteOnly);
            indexBuffer.SetData(indices);
        }

        public void Draw()
        {
            shader.CurrentTechnique = shader.Techniques["TexturedShadowedTerrain"];
            shader.Parameters["World"].SetValue(Matrix.CreateTranslation(-.5f, -1, -.5f));
            shader.Parameters["Texture1"].SetValue(texture1);
            shader.Parameters["Texture2"].SetValue(texture2);
            shader.Parameters["Texture3"].SetValue(texture3);

            foreach (EffectPass pass in shader.CurrentTechnique.Passes)
            {
                pass.Apply();
                graphics.GraphicsDevice.Indices = indexBuffer;
                graphics.GraphicsDevice.SetVertexBuffer(vertexBuffer);
                graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Length, 0, indices.Length / 3);
            }
        }

        public void DrawShadow()
        {
            shader.CurrentTechnique = shader.Techniques["CreateShadow"];
            shader.Parameters["World"].SetValue(Matrix.CreateTranslation(-.5f, -1, -.5f));

            foreach (EffectPass pass in shader.CurrentTechnique.Passes)
            {
                pass.Apply();
                graphics.GraphicsDevice.Indices = indexBuffer;
                graphics.GraphicsDevice.SetVertexBuffer(vertexBuffer);
                graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertices.Length, 0, indices.Length / 3);
            }
        }

        public float getHeight(float x, float y)
        {
            int xInt = (int)x;
            int yInt = (int)y;

            return heightData[xInt, yInt] - 1;
        }

        public bool istBefahrbar(float x, float y)
        {
            int xInt = (int)x;
            int yInt = (int)y;

            return
                heightData[xInt, yInt] == heightData[xInt + 1, yInt] &&
                heightData[xInt, yInt] == heightData[xInt, yInt + 1] &&
                heightData[xInt, yInt] == heightData[xInt + 1, yInt + 1] &&
                heightData[xInt, yInt] > 0 &&
                heightData[xInt, yInt] <= 3;
        }

        public float GetRealHeight(float x, float y)
        {
            x += .5f;
            y += .5f;

            int xInt = (int)x;
            int yInt = (int)y;

            float xAmount = x - xInt;
            float yAmount = y - yInt;

            float untenHoehe = MathHelper.Lerp(
                heightData[xInt, yInt],
                heightData[xInt + 1, yInt],
                xAmount);

            float obenHoehe = MathHelper.Lerp(
                heightData[xInt, yInt + 1],
                heightData[xInt + 1, yInt + 1],
                xAmount);


            float hoehe = MathHelper.Lerp(
                untenHoehe,
                obenHoehe,
                yAmount);

            return hoehe - 1;
        }

        public Vector3 GetNormal(float x, float y)
        {
            x += .5f;
            y += .5f;

            int xInt = (int)x;
            int yInt = (int)y;

            float xAmount = x - xInt;
            float yAmount = y - yInt;
            
            Vector3 untenNormal = Vector3.Lerp(
                vertices[xInt + yInt * terrainWidth].Normal,
                vertices[xInt + 1 + yInt * terrainWidth].Normal,
                xAmount);

            Vector3 obenNormal = Vector3.Lerp(
                vertices[xInt + (yInt + 1) * terrainWidth].Normal,
                vertices[xInt + 1 + (yInt + 1) * terrainWidth].Normal,
                xAmount);

            Vector3 normal = Vector3.Lerp(untenNormal, obenNormal, yAmount);
            normal.Normalize();

            return normal;
        }

        public override void Load()
        {
        }

        public override void UnLoad()
        {
        }
    }
}
